//
//  TriangleShape.cpp
//  Ray Tracer
//
//  Created by Kaila, Ashish on 4/13/13.
//  Copyright (c) 2013 Kaila, Ashish. All rights reserved.
//

#include "TriangleShape.h"

TriangleShape::TriangleShape(Point3D firstVertex,
                             Point3D secondVertex,
                             Point3D thirdVertex,
                             const Material* material)
: SceneObject(material)
{
    _vertices[0] = firstVertex;
    _vertices[1] = secondVertex;
    _vertices[2] = thirdVertex;
    
    Vector3D BA = _vertices[0] - _vertices[1];
    Vector3D BC = _vertices[2] - _vertices[1];
    
    Vector3D n = BC.cross(BA);
    n.normalize();
    
    _normals[0] = n;
    _normals[1] = n;
    _normals[2] = n;
}

void TriangleShape::setNormals(unsigned int normalIndex, Vector3D& normal)
{
    if (normalIndex > 2)
    {
        return;
    }
    
    _normals[normalIndex] = normal;
}

// Intersection
bool TriangleShape::intersect(Ray3D& ray,
                                const Matrix4x4& worldToModel,
                                const Matrix4x4& modelToWorld) const
{
    Ray3D rayInObjectSpace = getRayInObjectSpace(ray, worldToModel);
    
    if (rayInObjectSpace.dir.length() == 0)
    {
        return false;
    }

    // NOTE: Vertices are already in object space
    
    // Let P = o + td then if p lies on the plane of triangle then:
    // (P - A) . n = 0, => (o + td) . n = A . n, => t = (A - o) . n / d . n
    
    Vector3D BA = _vertices[0] - _vertices[1];
    Vector3D BC = _vertices[2] - _vertices[1];
    
    Vector3D n = BC.cross(BA);
    n.normalize();
    
    // This t yields the point where ray intersects the plane of triangle
    double t = (_vertices[0] - rayInObjectSpace.origin).dot(n) / (rayInObjectSpace.dir.dot(n));
    
    if (t < 0)
    {
        return false;
    }
    
    Point3D intersectionPoint = rayInObjectSpace.origin + t * rayInObjectSpace.dir;
    
    if (contains(intersectionPoint))
    {
        if ((ray.intersection.none) || (ray.intersection.t_value > t))
        {
            // Interpolate normal to the point
            double a = 0, b = 0, c = 0;
            calculateBarycentricCoordinates(intersectionPoint, a, b, c);
            ray.intersection.none = false;
            ray.intersection.normal = transNorm(worldToModel, a * _normals[0] + b * _normals[1] + c * _normals[2]);
            ray.intersection.normal.normalize();
            ray.intersection.point = modelToWorld * intersectionPoint;
            ray.intersection.pointInObjectSpace = intersectionPoint;
            ray.intersection.t_value = t;
            ray.intersection.obj = this;
            return true;
        }
    }
    
    return false;
}

// Returns true if a point in an object space lies within the triangle
bool TriangleShape::contains(const Point3D& objectInPointSpace) const
{
    Vector3D BA = _vertices[0] - _vertices[1];
    Vector3D BC = _vertices[2] - _vertices[1];
    
    Vector3D n = BC.cross(BA);
    n.normalize();
    
    // Perform an anticlock winding to ensure point lies within the triangle
    Point3D p0 = _vertices[0];
    Point3D p1 = _vertices[1];
    Point3D p2 = _vertices[2];
    
    double i = (p1 - p0).cross(objectInPointSpace - p0).dot(n);
    double j = (p2 - p1).cross(objectInPointSpace - p1).dot(n);
    double k = (p0 - p2).cross(objectInPointSpace - p2).dot(n);    
    
    return ((i >= 0) && (j >= 0) && (k >=0));
}

// Returns true if world point is contained within the object
bool TriangleShape::contains(const Point3D& worldPoint, const Matrix4x4& worldToModel) const
{
    return contains(worldToModel * worldPoint);
}

// Maps the texture points
void TriangleShape::setTexturePoint(unsigned int vertexIndex, const double texturePointX, const double texturePointY)
{
    if (vertexIndex > 2)
    {
        return;
    }
    
    _texturePoints[vertexIndex] = std::make_pair(texturePointX, texturePointY);
}

double calculateTriangleArea(Point3D a, Point3D b, Point3D c)
{
    return fabs(0.5 * (b - a).cross(c - a).length());
}

// Returns the texture mapper for the shape
const ITextureMapper& TriangleShape::getTextureMapper() const
{
    return *this;
}

// Calculates barycentric coordinates for a given point
void TriangleShape::calculateBarycentricCoordinates(Point3D point, double& a, double& b, double&c) const
{
    // Calculate barycentric coordinates
    a = calculateTriangleArea(_vertices[1], _vertices[2], point);
    b = calculateTriangleArea(_vertices[2], _vertices[0], point);
    c = calculateTriangleArea(_vertices[0], _vertices[1], point);
    
    double totalArea = a + b + c;
    
    a /= totalArea;
    b /= totalArea;
    c /= totalArea;
}

bool TriangleShape::mapTexture(const Intersection& intersection, double& i, double& j) const
{
    // Calculate barycentric coordinates
    double a;
    double b;
    double c;
    
    calculateBarycentricCoordinates(intersection.pointInObjectSpace, a, b, c);
    
    i = a * _texturePoints[0].first + b * _texturePoints[1].first + c * _texturePoints[2].first;
    j = a * _texturePoints[0].second + b * _texturePoints[1].second + c * _texturePoints[2].second;
    
    if (i > 1)
    {
        i = fabs(i) - (int)fabs(i);
    }
    
    if (i < 0)
    {
        i = 1 + i;
    }
    
    if (j > 1)
    {
        j = fabs(j) - (int)fabs(j);
    }
    
    if (j < 0)
    {
        j = 1 + j;
    }
    return true;
}